package com.access.boot.autoconfigure.apisdk.util;

import com.access.boot.apisdk.SdkContext;
import com.access.boot.autoconfigure.apisdk.exception.JavassistException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.AttributeInfo;
import javassist.bytecode.BadBytecode;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.ParameterAnnotationsAttribute;
import javassist.bytecode.SignatureAttribute;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.AnnotationMemberValue;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.BooleanMemberValue;
import javassist.bytecode.annotation.ByteMemberValue;
import javassist.bytecode.annotation.CharMemberValue;
import javassist.bytecode.annotation.ClassMemberValue;
import javassist.bytecode.annotation.DoubleMemberValue;
import javassist.bytecode.annotation.EnumMemberValue;
import javassist.bytecode.annotation.FloatMemberValue;
import javassist.bytecode.annotation.IntegerMemberValue;
import javassist.bytecode.annotation.LongMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.ShortMemberValue;
import javassist.bytecode.annotation.StringMemberValue;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * @author Yangzhen
 * @date 2022/6/8 15:52
 * @since 1.0.0
 */
public class EnhanceUtil {

    public static final ClassPool pool = ClassPool.getDefault();

    public static final String SDK_CONTEXT_CLASS = SdkContext.class.getName();


    public static final Map<String, Class<?>> PRIMITIVE_MAPPING = new HashMap<>();

    static {
        PRIMITIVE_MAPPING.put("int", int.class);
        PRIMITIVE_MAPPING.put("double", double.class);
        PRIMITIVE_MAPPING.put("long", long.class);
        PRIMITIVE_MAPPING.put("short", short.class);
        PRIMITIVE_MAPPING.put("byte", byte.class);
        PRIMITIVE_MAPPING.put("boolean", boolean.class);
        PRIMITIVE_MAPPING.put("char", char.class);
        PRIMITIVE_MAPPING.put("float", float.class);
    }

    public static void main(String[] args) {

    }

    // 创建指定类的虚拟类
    public static CtClass copyCtClass(Class openApiInterface) {
        String packageName = openApiInterface.getPackage().getName();
        String delegatedConnectorName = "$" + openApiInterface.getSimpleName();
        String serviceName = Objects.nonNull(packageName) ? packageName + "." + delegatedConnectorName : delegatedConnectorName;
        CtClass targetCtClass = EnhanceUtil.pool.makeInterface(serviceName);
        return targetCtClass;
    }

    public static void copyParameterAnnotationsAttribute(ConstPool constPool, MethodInfo methodInfo, AttributeInfo attribute, boolean needAutoType) {
        if (attribute instanceof ParameterAnnotationsAttribute) {
            ParameterAnnotationsAttribute parameterAnnotations = (ParameterAnnotationsAttribute) attribute;
            Annotation[][] sourceAnnotations = parameterAnnotations.getAnnotations();
            Annotation[][] targetAnnotations;
            if (needAutoType) {
                int length = sourceAnnotations.length;
                targetAnnotations = getTargetAnnotations(constPool, sourceAnnotations, 1);
                targetAnnotations[length] = new Annotation[0];
            } else {
                targetAnnotations = getTargetAnnotations(constPool, sourceAnnotations, 0);
            }
            ParameterAnnotationsAttribute parameterAnnotationsAttribute = new ParameterAnnotationsAttribute(constPool, ParameterAnnotationsAttribute.visibleTag);
            parameterAnnotationsAttribute.setAnnotations(targetAnnotations);
            methodInfo.addAttribute(parameterAnnotationsAttribute);
        }
    }

    public static void addParameterAnnotationsAttribute(ConstPool constPool, MethodInfo methodInfo) {
        ParameterAnnotationsAttribute attribute = (ParameterAnnotationsAttribute) methodInfo.getAttribute(ParameterAnnotationsAttribute.visibleTag);
        ParameterAnnotationsAttribute parameterAnnotations = (ParameterAnnotationsAttribute) attribute;
        Annotation[][] sourceAnnotations = parameterAnnotations.getAnnotations();
        Annotation[][] targetAnnotations;
        int length = sourceAnnotations.length;
        targetAnnotations = getTargetAnnotations(constPool, sourceAnnotations, 1);
        targetAnnotations[length] = new Annotation[0];
        ParameterAnnotationsAttribute parameterAnnotationsAttribute = new ParameterAnnotationsAttribute(constPool, ParameterAnnotationsAttribute.visibleTag);
        parameterAnnotationsAttribute.setAnnotations(targetAnnotations);
        methodInfo.addAttribute(parameterAnnotationsAttribute);
    }

    private static Annotation[][] getTargetAnnotations(ConstPool constPool, Annotation[][] sourceAnnotations, int num) {
        int length = sourceAnnotations.length;
        int totalLength = length + num;
        Annotation[][] targetAnnotations = new Annotation[totalLength][];
        for (int i = 0; i < length; i++) {
            targetAnnotations[i] = new Annotation[sourceAnnotations[i].length];
            for (int j = 0; j < targetAnnotations[i].length; j++) {
                targetAnnotations[i][j] = new Annotation(sourceAnnotations[i][j].getTypeName(), constPool);
                copyMemberValue(constPool, sourceAnnotations[i][j], targetAnnotations[i][j]);
            }
        }
        return targetAnnotations;
    }

    public static void copyMemberValue(ConstPool constPool, Annotation sourceAnnotation, Annotation targetAnnotation) {
        Set<String> memberNames = sourceAnnotation.getMemberNames();
        if (memberNames != null) {
            Iterator<String> iterator = memberNames.iterator();
            while (iterator.hasNext()) {
                String memberName = iterator.next();
                MemberValue memberValue = sourceAnnotation.getMemberValue(memberName);
                MemberValue resultMemberValue = null;
                if (memberValue instanceof StringMemberValue) {
                    resultMemberValue = new StringMemberValue(((StringMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof BooleanMemberValue) {
                    resultMemberValue = new BooleanMemberValue(((BooleanMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof EnumMemberValue) {
                    EnumMemberValue enumMemberValue = (EnumMemberValue) memberValue;
                    EnumMemberValue newEnumMemberValue = new EnumMemberValue(constPool);
                    newEnumMemberValue.setType(enumMemberValue.getType());
                    newEnumMemberValue.setValue(enumMemberValue.getValue());
                    resultMemberValue = newEnumMemberValue;
                } else if (memberValue instanceof ClassMemberValue) {
                    resultMemberValue = new ClassMemberValue(((ClassMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof AnnotationMemberValue) {
                    resultMemberValue = new AnnotationMemberValue(((AnnotationMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof ByteMemberValue) {
                    resultMemberValue = new ByteMemberValue(((ByteMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof ShortMemberValue) {
                    resultMemberValue = new ShortMemberValue(((ShortMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof CharMemberValue) {
                    resultMemberValue = new CharMemberValue(((CharMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof IntegerMemberValue) {
                    resultMemberValue = new IntegerMemberValue(((IntegerMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof FloatMemberValue) {
                    resultMemberValue = new FloatMemberValue(((FloatMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof LongMemberValue) {
                    resultMemberValue = new LongMemberValue(((LongMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof DoubleMemberValue) {
                    resultMemberValue = new DoubleMemberValue(((DoubleMemberValue) memberValue).getValue(), constPool);
                } else if (memberValue instanceof ArrayMemberValue) {
                    ArrayMemberValue arrayMemberValue = (ArrayMemberValue) memberValue;
                    ArrayMemberValue newArrayMemberValue = new ArrayMemberValue(arrayMemberValue.getType(), constPool);
                    newArrayMemberValue.setValue(arrayMemberValue.getValue());
                    resultMemberValue = newArrayMemberValue;
                } else {
                    throw new JavassistException("not match any MemberValue, member name: " + memberName);
                }
                targetAnnotation.addMemberValue(memberName, resultMemberValue);
            }
        }
    }

    public static ParameterAnnotationsAttribute copyParameterAnnotationsAttribute(ConstPool cp, Method method, int appendNum) {
        ParameterAnnotationsAttribute oldAns = new ParameterAnnotationsAttribute(cp, ParameterAnnotationsAttribute.visibleTag);
        Annotation[][] anAr = new Annotation[method.getParameterAnnotations().length + appendNum][];
        for (int i = 0; i < anAr.length - appendNum; ++i) {
            final int length = method.getParameterAnnotations()[i].length;
            anAr[i] = new Annotation[length];
            for (int j = 0; j < anAr[i].length; ++j) {
                anAr[i][j] = createJavassistAnnotation(method.getParameterAnnotations()[i][j], cp);
            }
        }

        for (int i = anAr.length - appendNum; i < anAr.length; i++) {
            anAr[i] = new Annotation[0];
        }

        oldAns.setAnnotations(anAr);
        return oldAns;
    }

    public static ParameterAnnotationsAttribute copyParameterAnnotationsAttribute(ConstPool cp, Method method) {
        return copyParameterAnnotationsAttribute(cp, method, 0);
    }

    public static Annotation createJavassistAnnotation(java.lang.annotation.Annotation annotation, ConstPool cp) {
        try {
            Annotation newAnnotation = new Annotation(annotation.annotationType().getName(), cp);
            for (Method m : annotation.annotationType().getDeclaredMethods()) {
                Object val = m.invoke(annotation);
                newAnnotation.addMemberValue(m.getName(), createMemberValue(m.getReturnType(), val, cp));
            }
            return newAnnotation;
        } catch (Exception e) {
            throw new JavassistException(e);
        }
    }

    private static MemberValue createMemberValue(Class<?> type, Object val, ConstPool cp) {
        if (type == String.class) {
            return new StringMemberValue((String) val, cp);
        } else {
            throw new JavassistException("Only support string param value! Invalid param value type: " + type + " and value: " + val);
        }
    }

    /**
     * 获取方法返回值类型，如果是范型，则返回范型类型
     *
     * @param method
     * @return
     */
    public static String inferMethodTypeName(CtMethod method) {
        try {
            if (method.getMethodInfo2().getAttribute(SignatureAttribute.tag) == null) {
                return method.getReturnType().getName();
            }
            return inferGenericTypeName(
                    method.getReturnType(),
                    SignatureAttribute.toMethodSignature(method.getGenericSignature()).getReturnType()
            );
        } catch (BadBytecode ignore) {
            return null;
        } catch (NotFoundException e) {
            return null;
        }
    }

    /**
     * 获取方法参数名称
     * 前置条件：
     * 代码编译时使用 -g 参数
     * 接口获取不到
     * <p>
     * javac -g Xxx.java
     * <plugin>
     * <groupId>org.apache.maven.plugins</groupId>
     * <artifactId>maven-compiler-plugin</artifactId>
     * <version>3.8.1</version>
     * <configuration>
     * <source>${java.version}</source>
     * <target>${java.version}</target>
     * <parameters>true</parameters>
     * <compilerArgs>
     * <compilerArg>-g</compilerArg>
     * </compilerArgs>
     * </configuration>
     * </plugin>
     *
     * @param method
     * @return
     * @throws NotFoundException
     */
    public static String[] getMethodParameterNames(CtMethod method) throws NotFoundException {
        CodeAttribute codeAttribute = method.getMethodInfo2().getCodeAttribute();
        if (codeAttribute != null) {
            LocalVariableAttribute attr =
                    (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            if (attr == null) {
                throw new IllegalStateException("LocalVariableAttribute is null");
            }

            String[] paramNames = new String[method.getParameterTypes().length];
            int offset = Modifier.isStatic(method.getModifiers()) ? 0 : 1;
            for (int i = 0; i < paramNames.length; i++) {
                paramNames[i] = attr.variableName(i + offset);
            }
            return paramNames;
        }
        return null;
    }

    /**
     * 推断方法返回值类型
     *
     * @param ctClass
     * @param genericSignature
     * @return
     */
    public static String inferGenericTypeName(CtClass ctClass, SignatureAttribute.Type genericSignature) {
        // infer targetEntity from generic type signature
        if (isAssignable(ctClass, Collection.class.getName())) {
            return ((SignatureAttribute.ClassType) genericSignature).getTypeArguments()[0].getType().jvmTypeName();
        }
        if (isAssignable(ctClass, Map.class.getName())) {
            return ((SignatureAttribute.ClassType) genericSignature).getTypeArguments()[1].getType().jvmTypeName();
        }
        return ctClass.getName();
    }

    public static boolean isAssignable(CtClass thisCtClass, String targetClassName) {
        if (thisCtClass == null) {
            return false;
        }
        if (thisCtClass.getName().equals(targetClassName)) {
            return true;
        }

        try {
            // check if extends
            if (isAssignable(thisCtClass.getSuperclass(), targetClassName)) {
                return true;
            }
            // check if implements
            for (CtClass interfaceCtClass : thisCtClass.getInterfaces()) {
                if (isAssignable(interfaceCtClass, targetClassName)) {
                    return true;
                }
            }
        } catch (NotFoundException e) {
            // keep going
        }
        return false;
    }

    public static boolean isAssignable(CtField thisCtField, String targetClassName) {
        try {
            return isAssignable(thisCtField.getType(), targetClassName);
        } catch (NotFoundException e) {
            // keep going
        }
        return false;
    }

    /**
     * 新建方法签名
     *
     * @param sourceCtMethod
     * @param returnType
     * @return
     * @throws NotFoundException
     */
    public static String newMethodSignature(CtMethod sourceCtMethod, CtClass returnType) throws NotFoundException {
        String actualType = inferMethodTypeName(sourceCtMethod);
        CtClass actualCtClass = pool.getCtClass(actualType);
        return newMethodSignature(actualCtClass.getName(), returnType.getName());
    }

    /**
     * 新建方法签名
     *
     * @param genericReturnType
     * @return
     * @throws NotFoundException
     */
    public static String newMethodSignature(Type genericReturnType) throws NotFoundException {
        ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
        Type rawType = parameterizedType.getRawType();
        String actualType = parameterizedType.getActualTypeArguments()[0].getTypeName();
        return newMethodSignature(actualType, rawType.getTypeName());
    }

    /**
     * 新建方法签名
     *
     * @param actualType
     * @param rawType
     * @return
     */
    public static String newMethodSignature(String actualType, String rawType) {
        SignatureAttribute.MethodSignature methodSignature = new SignatureAttribute.MethodSignature(
                new SignatureAttribute.TypeParameter[0],
                new SignatureAttribute.Type[]{new SignatureAttribute.ClassType(actualType)},
                new SignatureAttribute.ClassType(
                        rawType,
                        new SignatureAttribute.TypeArgument[]{
                                new SignatureAttribute.TypeArgument(new SignatureAttribute.ClassType(actualType))
                        }
                ),
                new SignatureAttribute.ObjectType[0]
        );
        return methodSignature.encode();
    }
}
